Create your own browser-based Snake Game using HTML, CSS, and JavaScript with our step-by-step tutorial. Play the classic game and add features to make it more interesting.
Table of Contents
Welcome to the classic Snake game, now available to play in your browser! This game has been a fan favorite for decades, and now you can experience all the retro fun right in your web browser. Using the latest web technologies such as HTML, CSS, and JavaScript, we've built a custom version of Snake that is sure to bring back memories and provide hours of entertainment.
In this game, your goal is to control the snake and navigate it through the playing field, collecting food along the way. Be careful not to crash into your own body, as this will end the game. As you progress, the snake will grow longer and the game will become more challenging.
With simple controls and addictive gameplay, this Snake game is sure to keep you entertained for hours. So get ready to relive the classic Snake experience, now available in your browser thanks to HTML, CSS, and JavaScript. Let the retro fun begin!
Let's start making an amazing classic snake game made Using HTML, CSS and JavaScript step by step.
Source Code
Step 1 (HTML Code):
To get started, we will first need to create a basic HTML file. The HTML is structured with a container element that contains the wrapper element, which in turn contains the canvas element, button, and score display. There is also an element for displaying the game's title and author.
The button uses an icon from Font Awesome and has an ID of "replay" that can be used to identify it in the JavaScript code. The canvas element has an ID of "canvas" and the score display has an ID of "score". These IDs can also be used to manipulate the elements in the JavaScript code.
After creating the files just paste the following codes into your file. Remember that you must save a file with the .html extension.
Step 2 (CSS Code):
Next, we will create our CSS file. This is the CSS code for styling the Snake game. It includes styles for various elements such as the body, canvas, buttons, and score display. The styles include setting font families, colors, sizes, and other visual properties.
The code uses the @font-face rule to define a custom font family called "game" and link it to a Google Fonts stylesheet. It also uses the * selector to reset the padding and margin for all elements, as well as the box-sizing property to ensure that the width and height of elements include their padding and border.There are also styles for the body element, including setting the background color and aligning the content in the center of the page. The canvas element has a background color, and there are styles for the container, wrapper, and score display elements to control their layout and appearance.
There are also styles for the replay button, including setting the font size, padding, background color, border, and border radius. There are also styles for the hover state of the button, which changes the background color and adds a transition effect.
Finally, there are styles for the author element at the bottom of the page, including setting the font size, weight, and color, as well as the letter spacing. There are also media queries to adjust the layout and styles for smaller screen sizes.
This will give our snake game an upgraded presentation. Create a CSS file with the name of styles.css and paste the given codes into your CSS file. Remember that you must create a file with the .css extension.
@font-face {
font-family: "game";
src: url("https://fonts.googleapis.com/css2?family=Poppins:wght@500;800&display=swap");
}
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
button:focus {
outline: 0;
}
html,
body {
height: 100%;
font-family: "Poppins", sans-serif;
color: #6e7888;
}
body {
background-color: #222738;
display: flex;
justify-content: center;
align-items: center;
color: #6e7888;
}
canvas {
background-color: #181825;
}
.container {
display: flex;
width: 100%;
height: 100%;
flex-flow: column wrap;
justify-content: center;
align-items: center;
}
#ui {
display: flex;
align-items: center;
font-size: 10px;
flex-flow: column;
margin-left: 10px;
}
h2 {
font-weight: 200;
transform: rotate(270deg);
}
#score {
margin-top: 20px;
font-size: 30px;
font-weight: 800;
}
.noselect {
user-select: none;
}
#replay {
font-size: 10px;
padding: 10px 20px;
background: #6e7888;
border: none;
color: #222738;
border-radius: 20px;
font-weight: 800;
transform: rotate(270deg);
cursor: pointer;
transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
}
#replay:hover {
background: #a6aab5;
background: #4cffd7;
transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
}
#replay svg {
margin-right: 8px;
}
@media (max-width: 600px) {
#replay {
margin-bottom: 20px;
}
#replay,
h2 {
transform: rotate(0deg);
}
#ui {
flex-flow: row wrap;
margin-bottom: 20px;
}
#score {
margin-top: 0;
margin-left: 20px;
}
.container {
flex-flow: column wrap;
}
}
#author {
width: 100%;
bottom: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 600;
color: inherit;
text-transform: uppercase;
padding-left: 35px;
}
#author span {
font-size: 10px;
margin-left: 20px;
color: inherit;
letter-spacing: 4px;
}
#author h1 {
font-size: 25px;
}
.wrapper {
display: flex;
flex-flow: row wrap;
justify-content: center;
align-items: center;
margin-bottom: 20px;
}
Step 3 (JavaScript Code):
Finally, we need to create a function in JavaScript. It includes variables for storing references to DOM elements, such as the replay button, score display, and canvas element. It also includes variables for storing game state and data, such as the snake, food, current hue, cell size, and game over status.
The code defines a number of helper functions and classes, such as the Vec class for representing 2D vectors and the isCollision function for checking if two vectors are equal. It also includes functions for drawing the game grid, generating a random hue, and converting a hue, saturation, and lightness value to an RGB color.
The code also includes event listeners for handling user input, such as clicks on the replay button. It includes a function for updating the game state, as well as functions for rendering the game, drawing the snake and food, and handling collisions.
The code includes a game loop that uses the requestAnimationFrame function to update and render the game at a consistent frame rate.
Create a JavaScript file with the name of script.js and paste the given codes into your JavaScript file. Remember, you’ve to create a file with .js extension.
let dom_replay = document.querySelector("#replay");
let dom_score = document.querySelector("#score");
let dom_canvas = document.createElement("canvas");
document.querySelector("#canvas").appendChild(dom_canvas);
let CTX = dom_canvas.getContext("2d");
const W = (dom_canvas.width = 400);
const H = (dom_canvas.height = 400);
let snake,
food,
currentHue,
cells = 20,
cellSize,
isGameOver = false,
tails = [],
score = 00,
maxScore = window.localStorage.getItem("maxScore") || undefined,
particles = [],
splashingParticleCount = 20,
cellsCount,
requestID;
let helpers = {
Vec: class {
constructor(x, y) {
this.x = x;
this.y = y;
}
add(v) {
this.x += v.x;
this.y += v.y;
return this;
}
mult(v) {
if (v instanceof helpers.Vec) {
this.x *= v.x;
this.y *= v.y;
return this;
} else {
this.x *= v;
this.y *= v;
return this;
}
}
},
isCollision(v1, v2) {
return v1.x == v2.x && v1.y == v2.y;
},
garbageCollector() {
for (let i = 0; i < particles.length; i++) {
if (particles[i].size <= 0) {
particles.splice(i, 1);
}
}
},
drawGrid() {
CTX.lineWidth = 1.1;
CTX.strokeStyle = "#232332";
CTX.shadowBlur = 0;
for (let i = 1; i < cells; i++) {
let f = (W / cells) * i;
CTX.beginPath();
CTX.moveTo(f, 0);
CTX.lineTo(f, H);
CTX.stroke();
CTX.beginPath();
CTX.moveTo(0, f);
CTX.lineTo(W, f);
CTX.stroke();
CTX.closePath();
}
},
randHue() {
return ~~(Math.random() * 360);
},
hsl2rgb(hue, saturation, lightness) {
if (hue == undefined) {
return [0, 0, 0];
}
var chroma = (1 - Math.abs(2 * lightness - 1)) * saturation;
var huePrime = hue / 60;
var secondComponent = chroma * (1 - Math.abs((huePrime % 2) - 1));
huePrime = ~~huePrime;
var red;
var green;
var blue;
if (huePrime === 0) {
red = chroma;
green = secondComponent;
blue = 0;
} else if (huePrime === 1) {
red = secondComponent;
green = chroma;
blue = 0;
} else if (huePrime === 2) {
red = 0;
green = chroma;
blue = secondComponent;
} else if (huePrime === 3) {
red = 0;
green = secondComponent;
blue = chroma;
} else if (huePrime === 4) {
red = secondComponent;
green = 0;
blue = chroma;
} else if (huePrime === 5) {
red = chroma;
green = 0;
blue = secondComponent;
}
var lightnessAdjustment = lightness - chroma / 2;
red += lightnessAdjustment;
green += lightnessAdjustment;
blue += lightnessAdjustment;
return [
Math.round(red * 255),
Math.round(green * 255),
Math.round(blue * 255)
];
},
lerp(start, end, t) {
return start * (1 - t) + end * t;
}
};
let KEY = {
ArrowUp: false,
ArrowRight: false,
ArrowDown: false,
ArrowLeft: false,
resetState() {
this.ArrowUp = false;
this.ArrowRight = false;
this.ArrowDown = false;
this.ArrowLeft = false;
},
listen() {
addEventListener(
"keydown",
(e) => {
if (e.key === "ArrowUp" && this.ArrowDown) return;
if (e.key === "ArrowDown" && this.ArrowUp) return;
if (e.key === "ArrowLeft" && this.ArrowRight) return;
if (e.key === "ArrowRight" && this.ArrowLeft) return;
this[e.key] = true;
Object.keys(this)
.filter((f) => f !== e.key && f !== "listen" && f !== "resetState")
.forEach((k) => {
this[k] = false;
});
},
false
);
}
};
class Snake {
constructor(i, type) {
this.pos = new helpers.Vec(W / 2, H / 2);
this.dir = new helpers.Vec(0, 0);
this.type = type;
this.index = i;
this.delay = 5;
this.size = W / cells;
this.color = "white";
this.history = [];
this.total = 1;
}
draw() {
let { x, y } = this.pos;
CTX.fillStyle = this.color;
CTX.shadowBlur = 20;
CTX.shadowColor = "rgba(255,255,255,.3 )";
CTX.fillRect(x, y, this.size, this.size);
CTX.shadowBlur = 0;
if (this.total >= 2) {
for (let i = 0; i < this.history.length - 1; i++) {
let { x, y } = this.history[i];
CTX.lineWidth = 1;
CTX.fillStyle = "rgba(225,225,225,1)";
CTX.fillRect(x, y, this.size, this.size);
}
}
}
walls() {
let { x, y } = this.pos;
if (x + cellSize > W) {
this.pos.x = 0;
}
if (y + cellSize > W) {
this.pos.y = 0;
}
if (y < 0) {
this.pos.y = H - cellSize;
}
if (x < 0) {
this.pos.x = W - cellSize;
}
}
controlls() {
let dir = this.size;
if (KEY.ArrowUp) {
this.dir = new helpers.Vec(0, -dir);
}
if (KEY.ArrowDown) {
this.dir = new helpers.Vec(0, dir);
}
if (KEY.ArrowLeft) {
this.dir = new helpers.Vec(-dir, 0);
}
if (KEY.ArrowRight) {
this.dir = new helpers.Vec(dir, 0);
}
}
selfCollision() {
for (let i = 0; i < this.history.length; i++) {
let p = this.history[i];
if (helpers.isCollision(this.pos, p)) {
isGameOver = true;
}
}
}
update() {
this.walls();
this.draw();
this.controlls();
if (!this.delay--) {
if (helpers.isCollision(this.pos, food.pos)) {
incrementScore();
particleSplash();
food.spawn();
this.total++;
}
this.history[this.total - 1] = new helpers.Vec(this.pos.x, this.pos.y);
for (let i = 0; i < this.total - 1; i++) {
this.history[i] = this.history[i + 1];
}
this.pos.add(this.dir);
this.delay = 5;
this.total > 3 ? this.selfCollision() : null;
}
}
}
class Food {
constructor() {
this.pos = new helpers.Vec(
~~(Math.random() * cells) * cellSize,
~~(Math.random() * cells) * cellSize
);
this.color = currentHue = `hsl(${~~(Math.random() * 360)},100%,50%)`;
this.size = cellSize;
}
draw() {
let { x, y } = this.pos;
CTX.globalCompositeOperation = "lighter";
CTX.shadowBlur = 20;
CTX.shadowColor = this.color;
CTX.fillStyle = this.color;
CTX.fillRect(x, y, this.size, this.size);
CTX.globalCompositeOperation = "source-over";
CTX.shadowBlur = 0;
}
spawn() {
let randX = ~~(Math.random() * cells) * this.size;
let randY = ~~(Math.random() * cells) * this.size;
for (let path of snake.history) {
if (helpers.isCollision(new helpers.Vec(randX, randY), path)) {
return this.spawn();
}
}
this.color = currentHue = `hsl(${helpers.randHue()}, 100%, 50%)`;
this.pos = new helpers.Vec(randX, randY);
}
}
class Particle {
constructor(pos, color, size, vel) {
this.pos = pos;
this.color = color;
this.size = Math.abs(size / 2);
this.ttl = 0;
this.gravity = -0.2;
this.vel = vel;
}
draw() {
let { x, y } = this.pos;
let hsl = this.color
.split("")
.filter((l) => l.match(/[^hsl()$% ]/g))
.join("")
.split(",")
.map((n) => +n);
let [r, g, b] = helpers.hsl2rgb(hsl[0], hsl[1] / 100, hsl[2] / 100);
CTX.shadowColor = `rgb(${r},${g},${b},${1})`;
CTX.shadowBlur = 0;
CTX.globalCompositeOperation = "lighter";
CTX.fillStyle = `rgb(${r},${g},${b},${1})`;
CTX.fillRect(x, y, this.size, this.size);
CTX.globalCompositeOperation = "source-over";
}
update() {
this.draw();
this.size -= 0.3;
this.ttl += 1;
this.pos.add(this.vel);
this.vel.y -= this.gravity;
}
}
function incrementScore() {
score++;
dom_score.innerText = score.toString().padStart(2, "0");
}
function particleSplash() {
for (let i = 0; i < splashingParticleCount; i++) {
let vel = new helpers.Vec(Math.random() * 6 - 3, Math.random() * 6 - 3);
let position = new helpers.Vec(food.pos.x, food.pos.y);
particles.push(new Particle(position, currentHue, food.size, vel));
}
}
function clear() {
CTX.clearRect(0, 0, W, H);
}
function initialize() {
CTX.imageSmoothingEnabled = false;
KEY.listen();
cellsCount = cells * cells;
cellSize = W / cells;
snake = new Snake();
food = new Food();
dom_replay.addEventListener("click", reset, false);
loop();
}
function loop() {
clear();
if (!isGameOver) {
requestID = setTimeout(loop, 1000 / 60);
helpers.drawGrid();
snake.update();
food.draw();
for (let p of particles) {
p.update();
}
helpers.garbageCollector();
} else {
clear();
gameOver();
}
}
function gameOver() {
maxScore ? null : (maxScore = score);
score > maxScore ? (maxScore = score) : null;
window.localStorage.setItem("maxScore", maxScore);
CTX.fillStyle = "#4cffd7";
CTX.textAlign = "center";
CTX.font = "bold 30px Poppins, sans-serif";
CTX.fillText("GAME OVER", W / 2, H / 2);
CTX.font = "15px Poppins, sans-serif";
CTX.fillText(`SCORE ${score}`, W / 2, H / 2 + 60);
CTX.fillText(`MAXSCORE ${maxScore}`, W / 2, H / 2 + 80);
}
function reset() {
dom_score.innerText = "00";
score = "00";
snake = new Snake();
food.spawn();
KEY.resetState();
isGameOver = false;
clearTimeout(requestID);
loop();
}
initialize();
Final Output:
Conclusion:
In conclusion, building and playing the classic Snake Game using HTML, CSS, and JavaScript is a fun and rewarding project for anyone interested in game development or web development. With the step-by-step guide provided in this blog post, you can easily build the game from scratch and add your own unique features to make it even more interesting. Whether you're a beginner or an experienced developer, this project is a great way to practice your skills and learn something new. So, give it a try and see what you can create!
That’s a wrap!
I hope you enjoyed this post. Now, with these examples, you can create your own amazing page.
Did you like it? Let me know in the comments below 🔥 and you can support me by buying me a coffee
And don’t forget to sign up to our email newsletter so you can get useful content like this sent right to your inbox!
Thanks!
Faraz 😊